Skip to content

drivers: usb: uhc_dwc2: add initial support#102967

Open
roma-jam wants to merge 2 commits into
zephyrproject-rtos:mainfrom
roma-jam-lab:pr-add-dwc2-uhc-channels-init
Open

drivers: usb: uhc_dwc2: add initial support#102967
roma-jam wants to merge 2 commits into
zephyrproject-rtos:mainfrom
roma-jam-lab:pr-add-dwc2-uhc-channels-init

Conversation

@roma-jam
Copy link
Copy Markdown
Contributor

@roma-jam roma-jam commented Jan 27, 2026

Description

This is initial support of Synopsys DWC2 controller as a USB Host (esp32 vendor quirks included, so it might be tested on esp32s3).

These changes allow to init dwc2 as a host and enumerate attached USB device and utilize the Control Endpoint to get device information (for example, via shell example command: usbh device info) for different devices.

nRF54LM20 support

With vendor quirks #95723 it works with Full- and High speed devices as well, so the code might be tested on the nRF54LM20-DK board.

Limitations

  • Not handling any errors during the transfer. If the transfer has finished with an error, there will be LOG_ERR() output followed by LOG_WRN() which reveals where handling is absent.
  • Uses only one channel - channel number 0. Additional logic to claim vacant channel on uhc_dwc2_enqueue will be added as a follow-up.
  • Supports only Control transfer. Bulk, Interrupt and Isochronous transfers will be added as a follow-up.
  • Supports only Buffer DMA mode (Slave or Scatter Gather also might be added, but esp32s3 and nRF54 both have DMA)
  • Does not synchronize cache (marked as TODO: in the code, where it should be done)
  • Does not support Hub connection (when device is a downstream device of the external hub)

And some other small limitations, that marked as TODOs in the code and can be solved in the future as a follow-up (with test coverage or without. "With" is better IMHO).

Testing with usbh shell on ESP32S3

  1. From the /samples/subsys/usb/shell folder, run
$ west build -b esp32s3_devkitc/esp32s3/procpu -- \
 -DCONFIG_SHELL=y \
 -DCONFIG_USB_HOST_STACK=y \
 -DCONFIG_USBH_SHELL=y \
 -DCONFIG_UDC_DWC2=n 

and get:

... 

Successfully created ESP32-S3 image.

then run:

$ west flash --esp-device /dev/ttyACM0 && west espressif monitor -p /dev/ttyACM0

and the the common usbh shell commands are available:

...

*** Booting Zephyr OS build 18c5216bfc70 ***
uart:~$ usbh init 
host: USB host initialized
uart:~$ usbh enable 
host: USB host enabled
[00:00:04.638,000] <inf> usbh_dev: New device with address 1 state 2
[00:00:04.640,000] <inf> usbh_dev: Configuration 1 bNumInterfaces 3
uart:~$ usbh device list 
1
uart:~$ usbh device info 1
Device Descriptor:
  bLength                        18
  bDescriptorType                 1
  bcdUSB                       2.00
  bDeviceClass                  239
  bDeviceSubClass                 2
  bDeviceProtocol                 1
  bMaxPacketSize0                64
  idVendor                   0x1E45
  idProduct                  0x8022
  bcdDevice                   58.01
  iManufacturer                   3
  iProduct                        1
  iSerial                         2
  bNumConfigurations              1
  Configuration Descriptor:
    bLength                         9
    bDescriptorType                 2
    wTotalLength               0x0144
    bNumInterfaces                  2
    bConfigurationValue             1
    iConfiguration                  4
    bmAttributes                 0x80
    bMaxPower                     500 mA
    Interface Association:
      bLength                         8
    ...

Related

@roma-jam roma-jam force-pushed the pr-add-dwc2-uhc-channels-init branch from 01ebd5f to f63106f Compare January 27, 2026 15:38
@josuah josuah linked an issue Jan 27, 2026 that may be closed by this pull request
@roma-jam roma-jam force-pushed the pr-add-dwc2-uhc-channels-init branch from f63106f to cbcf576 Compare January 28, 2026 08:38
@roma-jam roma-jam changed the title [WIP] drivers: usb: uhc: Added channels logic [part5] [WIP] drivers: usb: uhc: Added channels initial logic [part5] Jan 28, 2026
@roma-jam roma-jam force-pushed the pr-add-dwc2-uhc-channels-init branch 2 times, most recently from abe9038 to 3d06d02 Compare February 20, 2026 11:33
@roma-jam roma-jam force-pushed the pr-add-dwc2-uhc-channels-init branch 3 times, most recently from 1df627f to aae36b7 Compare March 2, 2026 13:04
@roma-jam roma-jam changed the title [WIP] drivers: usb: uhc: Added channels initial logic [part5] drivers: usb: uhc: Added channels initial logic [part5] Mar 2, 2026
@roma-jam roma-jam marked this pull request as ready for review March 2, 2026 13:22
@zephyrbot zephyrbot added area: USB Universal Serial Bus area: Samples Samples area: Devicetree Binding PR modifies or adds a Device Tree binding area: Xtensa Xtensa Architecture platform: ESP32 Espressif ESP32 labels Mar 2, 2026
@roma-jam roma-jam force-pushed the pr-add-dwc2-uhc-channels-init branch 4 times, most recently from 8727bd1 to 32a4575 Compare April 30, 2026 23:40
Comment thread drivers/usb/uhc/uhc_dwc2.c
@roma-jam roma-jam force-pushed the pr-add-dwc2-uhc-channels-init branch from 32a4575 to fbaa637 Compare May 1, 2026 13:04
Comment thread drivers/usb/uhc/uhc_dwc2.c
@roma-jam roma-jam force-pushed the pr-add-dwc2-uhc-channels-init branch 3 times, most recently from 4321f55 to 5c9e0be Compare May 1, 2026 13:49
Comment thread dts/bindings/usb/snps,dwc2.yaml
@josuah
Copy link
Copy Markdown
Contributor

josuah commented May 1, 2026

I think I do not have any other modification to propose than those already opened.

It works with nRF54LM20 on this branch, and I anticipate it would also work on other platforms.

The driver is clearly a lot more robust than anything I could be coming-up with myself, and fixed many bugs due to my lack of deep understanding of DWC2 beyond the manual.

Thank you once again for bearing in, and for all of your help and expertise on this driver!

I will pursue with ISO support on top of it and stay reactive, i.e. rebase the nRF54LM20 branch frequently to keep testing things still work as you go.

Comment thread drivers/usb/uhc/uhc_dwc2.c Outdated
@roma-jam
Copy link
Copy Markdown
Contributor Author

roma-jam commented May 4, 2026

Hi everyone,

this PR is ready for the review and the incoming code might be tested with esp32s3 or nrf54lm20.

For more details, please refer to the PR description.

Comment thread drivers/usb/uhc/uhc_dwc2.c Outdated
Copy link
Copy Markdown
Contributor

@josuah josuah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed #108457 has some bugfixes that could be integrated directly in this PR.

Comment thread drivers/usb/uhc/Kconfig.dwc2
Comment thread drivers/usb/uhc/uhc_dwc2.c Outdated
Comment thread drivers/usb/uhc/uhc_dwc2.c Outdated
Comment thread drivers/usb/uhc/Kconfig.dwc2 Outdated
Comment on lines +56 to +61
The reset signaling can be generated on any Hub or Host Controller port by request from
the USB System Software. The USB 2.0 specification requires that "the reset signaling must
be driven for a minimum of 10ms" (see USB 2.0 chapter 7.1.7.5 for more details).
After the reset, the hub port will transition to the Enabled state (refer to Section 11.5).

The default value is set to 30 ms to be safe.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

30 ms is below the minimum required for root ports (which is what we are dealing with here). The relevant excerpt here is:

As an additional requirement, Host Controllers and the USB System Software must ensure that resets issued to the root ports drive reset long enough to overwhelm any concurrent resume attempts by downstream devices. It is required that resets from root ports have a duration of at least 50 ms (TDRSTR). It is not required that this be 50 ms of continuous Reset signaling. However, if the reset is not continuous, the interval(s) between reset signaling must be less than 3 ms (TRHRSI), and the duration of each SE0 assertion must be at least 10 ms (TDRST).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is very correct note.

Comment thread drivers/usb/common/usb_dwc2_hw.h
roma-jam added 2 commits May 7, 2026 19:28
Different core revisions require different hanlding.
To have an option to get the revision and apply different
logic add the possibility to read masked revision from
GSNPSID register.

Signed-off-by: Roman Leonov <jam_roma@yahoo.com>
Note: Control transfers only

Add initial usb host driver for Synopsys DWC2 with vendor quirks.

Signed-off-by: Roman Leonov <jam_roma@yahoo.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 7, 2026


LOG_DBG("New device, %s-speed", uhc_dwc2_speed_str[speed]);

priv->has_device = 1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priv->has_device = 1; is set even if speed is not supported. Perhaps creating a copy of each in each case?

static const char *const uhc_dwc2_speed_str[] = {"High", "Full", "Low"};
uint32_t speed = dwc2_hal_get_speed(dwc2);

LOG_DBG("New device, %s-speed", uhc_dwc2_speed_str[speed]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOG here is accessing speed value before check if it is valid. What if you add in each case the proper log like:

LOG_DBG("New device, Low-speed");
LOG_DBG("New device, Full-speed");
LOG_DBG("New device, High-speed");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was the log message when I developed the driver.

But to be honest, it doesn't make a lot of sense to keep it any longer so we can simplify if just to print the register value of speed at low level drive and remove the uhc_dwc2_speed_str completely.

Also, keep the speed parsing log to the higher layer usbh.

config UHC_DWC2
bool "DWC2 USB host controller driver"
depends on DT_HAS_SNPS_DWC2_ENABLED
default y
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In PR description, you wrote that building needs -DCONFIG_UDC_DWC2=n. If that is the case, why not removing default y from here?

Copy link
Copy Markdown
Contributor

@tmon-nordic tmon-nordic May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about simply add depends on !CONFIG_UDC_DWC2 line for the time being? This could be removed once (if at all) there is dynamic role switching implemented.

Copy link
Copy Markdown
Contributor Author

@roma-jam roma-jam May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the uhc driver implementation, there is not strict dependency from UDC and it is possible to have both at the same time.
For example, there should be no problem in having both drivers complied, but not running at the same time. That means, we can switch between them, via uhc_init, uhc_enable, then uhc_disable, uhc_shutdown and then udc_init and udc_enable (but uhc_shutdown is not implemented yet).

I added the -DCONFIG_UDC_DWC2=n to the build command at a very early stage, when there was a conflict during the build (multiple definition of `__device_dts_ord_75').

Right now I know a bit more about the environment so I'll double check the conflict, maybe it is possible to make it right. But if it is a DT- conflict in utilizing the same peripheral (usb_otg), then let me know.

UPD: OK, seems that it is technically impossible. So seems that right now it gives us only two options: make them mutually exclusive or assign the node to UDC/UHC via menuconfig.

I'll think about it a bit more, but let me know if there are any preferences.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might only have been a problem before this was merged (which required manually turning off the device mode): #105005

As enabling host/device is still happening manually so it might work to enable both by default, and the user would then be responsible to enable host or device stack.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we enable both at the same time - we get the multiple definition of the node during the build.

I don't think (please, correct me if I am missing something) that changing the device tree description to having the assign of the role there something like this does make sense:

usb_otg: usb@40080000 {
    compatible = "snps,dwc2";
    reg = <0x40080000 0x40000>;
    interrupts = <...>;
    zephyr,usb-role = "host";
    status = "okay";
};

What about simply add depends on !CONFIG_UDC_DWC2 line for the time being?

So, I think that simple mutual exclusive like above is the best option at the moment.

Copy link
Copy Markdown
Contributor

@josuah josuah May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The devicetree is there to describe what exists in the hardware, and things like "role" is how this hardware is used by drivers, which I think is a different concern.

If a device needs multiple drivers (i.e. host and device mode), indeed it needs multiple devicetree nodes, such as having child nodes, one for device and one for host, both always enabled and Kconfig used to select what to leverage, as usual?

Though this seems a long-term issue, not needing a solution on this PR? Still glad to discuss it.

\
static struct esp32_usb_otg_data uhc_dwc2_quirk_data_##n; \
\
const struct uhc_dwc2_vendor_quirks uhc_dwc2_vendor_quirks_##n = { \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing static here.

gusbcfg &= ~(USB_DWC2_GUSBCFG_ULPIEVBUSD | USB_DWC2_GUSBCFG_ULPIEVBUSI);
/* Disable FS/LS ULPI and Suspend mode */
gusbcfg &= ~(USB_DWC2_GUSBCFG_ULPIFSLS | USB_DWC2_GUSBCFG_ULPICLK_SUSM);
/* Assigh the PHY clock for ULPI */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: Assign

/* Port disconnected */
/* Debounce port disconnection */
if (uhc_dwc2_port_debounce(dev, UHC_DWC2_EVENT_PORT_DISCONNECTION)) {
LOG_DBG("Port disconnected");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you loop over the channels and release it all in here?

for(...)
uhc_dwc2_channel_release(dev, ch);
uhc_xfer_return(dev, xfer, -ENODEV);

Same to the case of UHC_DWC2_EVENT_PORT_ERROR below..

/* TODO: Mask ACK, NAK, DTGERR */
} else {
/* TODO: Add handling for other cases */
LOG_WRN("IN not halted, unhandled HCINT 0x%08x", hcint);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

channel errors will be handled in another PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That depends on several things now.

I am proceeding with the ztest for uhc at the moment, so I can verify as many possible scenarios as I can and decrease the regression during the future changes.

Currently, the goal of this PR is to unlock the vendors to accomplish the quirks part if this is needed. Current state allows to test the peripheral and interact with USB device if there are no errors and USB device doesn't require any specific handling.

So, if till the moment I finish the tests, add all missing parts and close all TODOs before this PR is merged - I can include them in here as well.

sys_write32(hcfg, (mem_addr_t)&dwc2->hcfg);
}

static inline void dwc2_hal_config_hcfg(struct usb_dwc2_reg *const dwc2)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insight from the testing

Low speed devices doesn't work, because of FrInt wrong configuration for FSLS PHY.

There is not need to split configuration of HCFG and HFIR registers, because they control the same thing: host timings based on the port speed and PHY clock.

In this case, it is better to merge the configuration and provide something like dwc2_hal_config_timings and do that after port is enabled.

Information about the PHY might be gathered during dwc2_hal_init_gusbcfg and being kept in struct { bool is_hs; uint32_t clock_mhz; } phy; instead of simple uint32_t phy_clock_mhz;.

That will allowed to config timing in one place and both regosters together (HCFG and HFIR) avoiding the possible misconfiguration for different speed (such as for Low speed with FSLS PHY in the current code).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite grasp what you mean by bool is_hs. Do you mean there is need for dynamically switching the PHY used based on device connected?

UTMI+ PHY can just operate fine in either 30 MHz (for 16-bit) or 60 MHz (for 8-bit) mode regardless of what speed is the device connected. This does apply even when CONFIG_UHC_DRIVER_HIGH_SPEED_SUPPORT_ENABLED=n because then FSLSSupp would simply be 1 instead of 0. On nRF54LM20 I believe FSLSPclkSel should always be CLK3060 (0).

Copy link
Copy Markdown
Contributor Author

@roma-jam roma-jam May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean there is need for dynamically switching the PHY used based on device connected?

No, not dynamically.
This is a flag is_hs = usb_dwc2_get_ghwcfg2_hsphytype(ghwcfg2) != 0.
It is possible to remove it and read the ghwcfg2 instead.

And then, after the port is enabled, something like this:

        case USB_DWC2_HPRT_PRTSPD_HIGH:
		fslspclk = USB_DWC2_HCFG_FSLSPCLKSEL_CLK3060;
		frint = 125U * phy->clock_mhz - 1;
		break;

	case USB_DWC2_HPRT_PRTSPD_FULL:
		if (usb_dwc2_get_ghwcfg2_hsphytype(ghwcfg2) == 0) {
			fslspclk = USB_DWC2_HCFG_FSLSPCLKSEL_CLK48;
		} else {
			fslspclk = USB_DWC2_HCFG_FSLSPCLKSEL_CLK3060;
		}
		frint = 1000U * phy->clock_mhz - 1U;
		break;

	case USB_DWC2_HPRT_PRTSPD_LOW:
		fslspclk = USB_DWC2_HCFG_FSLSPCLKSEL_CLK6;
		frint = 1000U * 6U - 1U;
		break;

PS: CLK6 for Low speed is right for esp32, but it might be not right for nrf54.

UPD: I just tried the changes with FS and LS devices on the test (50 enumeration cycles) and yes it is possible to simplify it by removing the is_hs flag.
I will double check the nrf54 part, because it is not possible to enable/disable driver there to imitate disconnection, but once I am there I can run the tests on nrf54 as well.

ICYAI, I am talking about this tests: roma-jam-lab#7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code should rather be something like:

if (usb_dwc2_get_ghwcfg2_hsphytype(ghwcfg2) == USB_DWC2_GHWCFG2_HSPHYTYPE_NO_HS) {
  set fslspclk and clock_mhz based on port type
} else {
  fslspclk = USB_DWC2_HCFG_FSLSPCLKSEL_CLK3060;
  set clock_mhz to 30 Mhz if USB_DWC2_GUSBCFG_PHYIF_16_BIT else 60 Mhz
}

And then a switch for port type to determine frint value.

I will double check the nrf54 part, because it is not possible to enable/disable driver there to imitate disconnection, but once I am there I can run the tests on nrf54 as well.

What do you mean by "to imitate disconnection"? Do you just want to disable the host pulldowns?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code should rather be something like

This might be another option.

Currently, the initialization and configuration has been split in two parts:

  • initialization during the uhc_init(), when we configure the controller based on the used PHY and kept the PHY parameters (only clock is valid so far) till the port will be enabled
  • configuration after port was enabled. Here we apply the necessary config, based on the PHY clock and port speed.

I did that, because PHY can't be changed after init and we need to configure the registers as quick as possible. For me it seems that it fits your idea. Let me know if it doesn't.

What do you mean by "to imitate disconnection"?

Right now I use uhc_disable() to disable the controller, which turns of the port power and might be used as a simplified analogue for physical disconnection.

Copy link
Copy Markdown
Contributor Author

@roma-jam roma-jam May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @tmon-nordic,

it is not directly related, but thanks for the hint!
I checked the code and now Low-speed devices on nRF54LM20 are also working.

I will double check the changed code again, refactor it a bit, verify with tests and include in this PR during the next update iteration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: Devicetree Binding PR modifies or adds a Device Tree binding area: Samples Samples area: USB Universal Serial Bus area: Xtensa Xtensa Architecture platform: ESP32 Espressif ESP32

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DWC2 USB Host controller (UHC)

7 participants